﻿using System;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using SecretLabs.NETMF.Hardware.Netduino;
using System.Net.Sockets;
using System.Net;
using System.Text;
using Microsoft.SPOT.Net.NetworkInformation;

namespace NetduinoPlusTelnet
{

    /* Constant values defined to help readability throughout the code. */
    static class TSConst
    {
        // Constants for setting up the ConnectionHandler class.
        public const int RECEIVE_BUF_SIZE = 80; // Probably not getting more than a line of text at at time.
        public const int COMMAND_BUF_SIZE = 127; // Probably will not have any commands longer than this.
        public const int OPTION_BUF_SIZE = 20; // Subnegotiatiated session command options are usually small.

        // Receive buffer read modes - defined arbitrarily.
        public const int MODE_NORMAL_CMD = 1; // Read incoming buffer as plain command text.
        public const int MODE_SESSION_CMD = 2; // Read incoming buffer as a session command from client.
        public const int MODE_SESSION_CMD_CODE = 3; // Read incoming buffer as a session command code from client.
        public const int MODE_SESSION_OPTION = 4; // Read incoming buffer as a session command option from client.

        // Error strings for the user.
        public const string ERR_LONG_CMD = "The entered command was too long and has been ignored.\r\n";
        public const string ERR_BREAK = "Break received - Aborting.\r\n";
        public const string ERR_LONG_SESSION_CMD = "An excessive option string was received and has been ignored.\r\n";
        public const string ERR_INTERRUPT = "Control-C received - Aborting.\r\n";
        public const string ERR_BAD_AUTH = "% Authentication failed.\r\n";
        public const string ERR_BAD_EN = "% Access denied.\r\n\n";

        // A few constants to make the Telnet options and modes more readable.
        public const byte OPT_STATE = 0; // Field identifier for the option state array position.
        public const byte NEG_STATE = 1; // Field identifier for the option state array position.
        public const byte DISABLED = 0; // Option is currently disabled.
        public const byte ENABLED = 1; // Option is currently enabled.
        public const byte TELNET_WILL = 251; // The sender wants to start doing something.
        public const byte TELNET_WONT = 252; // The sender refuses to do something. 
        public const byte TELNET_DO = 253; // The sender instructs the other end to do something.
        public const byte TELNET_DONT = 254; // The sender forbids the other end from doing something.
        public const byte TELNET_NEG_AT_DEFAULT = 0; // Option has never been negotiated and is at default.
        public const byte TELNET_NEG_IN_PROGRESS = 1; // Option is being negotiated at present.
        public const byte TELNET_NEG_COMPLETE = 255; // Option has already been negotiated.

        // Security related constants.
        public const int FAIL_KICK_DELAY = 3000; // Delay in milliseconds before disconnecting for bad password attempts.

        // Need some version control here!
        public const string SW_VERSION = "0.0.93";
    }

    /* The actual Telnet Server. */
    public class TelnetServer
    {
        private int port;
        private int connectionBuffers;
        private int maximumConnections;
        private ConnectionHandler[] connHandlers;
        private int connTimeout;
        private Thread connDispatch = null;
        private Socket telnetSocket = null;

        /* Constructor for the TelnetServer class. */
        public TelnetServer(int timeout = 120, int listenPort = 23, int connBuffer = 1, int maxConn = 2)
        {
            // How many seconds to maintain an active, idle connection?
            connTimeout = timeout;

            // What port? 23 is the Telnet default, but for security might want to set MUCH higher.
            port = listenPort;

            // How many simultanious -NEW- connections will be handled?
            connectionBuffers = connBuffer;

            // How many concurrent -ACTIVE- connections will be handled?
            maximumConnections = maxConn;
        }

        // Property to read the total number of active telnet sessions.
        public int getTelnetSessionCount
        {
            get
            {
                // Active session count.
                int activeSessions = 0;

                foreach (ConnectionHandler c in connHandlers)
                {
                    if (!c.isAvailable()) activeSessions++;
                }

                return activeSessions;
            }
        }

        /* This method activates the Telnet Server. The object doesn't listen for connections until this is called. */
        public bool begin(bool sync = false)
        {

            // Usual stuff... Set up the Socket object. Some line like this will probably be in 90% of Netduino+ projects.
            telnetSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            // Ok... No flack about binding to all IP addresses. Trying to follow the KISS principle at least a little...
            telnetSocket.Bind(new IPEndPoint(IPAddress.Any, port));

            // Set up the listener with a buffer backlog value set to connectionBuffers.
            telnetSocket.Listen(connectionBuffers);

            // Load the connHandlers array with references to null ConnectionHandler objects.
            connHandlers = new ConnectionHandler[maximumConnections];

            // Construct the ConnectionHandler objects.
            for (int i = 0; i < maximumConnections; i++)
            {
                connHandlers[i] = new ConnectionHandler(connTimeout);
            }

            // Start up a thread to handle connection dispatching asynchronously, or don't if told not to.
            if (sync)
            {
                _connectionDispatcher();
            }
            else
            {
                connDispatch = new Thread(_connectionDispatcher);
                connDispatch.Start();
            }

            // Maybe I'll throw in error checking some day, but for now...
            return true;
        }

        /* This method will actually be a thread for accepting connections and dispatching them to the connection handler. */
        private void _connectionDispatcher()
        {
            // Dispatch connections forever.
            while (true)
            {
                // Check the status of all handlers.
                foreach (ConnectionHandler c in connHandlers)
                {
                    if (c.isAvailable())
                    {
                        // This handler is available. Listen for a new connection.
                        Socket newConnection = telnetSocket.Accept(); // Blocks thread until a connection is received.

                        // Got a connection! Pass it to the connection handler.
                        c.acceptConnection(newConnection);
                    }
                    else
                    {
                        // No handlers are available - Yield to other threads.
                        Thread.Sleep(0);
                    }
                }
            }
        }
    }

    public class ConnectionHandler
    {
        // ***** Define things that the telnet server will control here or before. *****
        //static OutputPort onBoardLED = new OutputPort(Pins.ONBOARD_LED, false);

        // Class control variables are here.
        private bool available; // Is this instance available to handle a connection?
        private bool connectionOpen; // Is the connection opened right now or has it been closed?
        private Socket activeConnection = null; // Current connection this class is handling.
        private Thread ispThread = null; // Incoming session processor thread.
        private int sessionTimeout; // How long a connection can be idle before disconnecting (in seconds).

        // Create the buffers for handling the incoming data.
        private byte[] receiveBuffer = new byte[TSConst.RECEIVE_BUF_SIZE]; // Raw data from client.
        private char[] cmdBuffer = new char[TSConst.COMMAND_BUF_SIZE]; // Parsed commands for execution.
        private byte[] optionBuffer = new byte[TSConst.OPTION_BUF_SIZE]; // Session command option buffer.
        private byte[] sessionCmd = new byte[2]; // 2-Byte pair represents a 1-byte command and a 1-byte option.
        private byte[] sendingCmd = new byte[3]; // 3-byte buffer for building up a session command.
        private byte[] byteToByteArray = new byte[1]; // Change a byte to a byte array by dropping it in.

        // Write position tracking, echo tracking, and consumed space tracking for the buffers.
        private int readByteCount; // How many bytes were loaded into the receiveBuffer.
        private int cmdBufWritePos; // The location the next byte should be added at in the command buffer.
        private int optBufWritePos; // The location the next byte should be added at in the option buffer.
        private bool echoTyping; // Should typed characters be echoed to the screen?

        // Mode control to help determine which buffer incoming data should go in.
        private int readMode; // Holds a mode constant as a buffer selector.
        private int previousReadMode; // Need this so we can handle commands that land in the middle of the option buffer read.

        // Telnet states - Save memory by only tracking the ones we support.
        private byte[] telnetStateCLE; // Telnet option state - Client Local Echo.
        private byte[] telnetStateSGA; // Telnet option state - Suppress Go Ahead.

        // Telnet session authentication state - logged in, etc.
        private bool authenticated; // Is the session authenticated?
        private int authTries; // How many tries has the user made to authenticate?
        private bool enabled; // Is the session in privileged mode?
        private int enTries; // How many tries has the user made to enter privileged mode?

        public ConnectionHandler(int timeout = 300)
        {
            // This is a new object... It is definitely available.
            available = true;

            // Set the connection timeout.
            sessionTimeout = timeout;
        }

        /* This method actually accepts the cocnnection from the dispatcher. Returns false of busy, otherwise true. */
        public bool acceptConnection(Socket incomingConnection)
        {
            // If the object is already handling a connection, it can't do anything.
            if (!available) return false;

            // And if not, it is now!
            available = false;

            // Set up the security state.
            authenticated = false;
            enabled = false;
            authTries = 0;
            enTries = 0;

            // Set up the telnet state variables.
            telnetStateCLE = new byte[2];
            telnetStateSGA = new byte[2];

            // Store the new connection.
            activeConnection = incomingConnection;

            // Mark the current connection as open so later we can avoid calling Close on a connection that already is.
            connectionOpen = true;

            // Start a thread for the interactive session processor to take care of the connection.
            ispThread = new Thread(_sessionProcessor);
            ispThread.Start();

            return true;
        }

        /* Can this handler instance accept a new connection or is it in use? */
        public bool isAvailable()
        {
            return available;
        }

        /* This method resets the state of this class instance for a new connection. */
        private void _resetTelnetOptionState()
        {
            // Set the option state variables back to defaults.
            telnetStateCLE[TSConst.OPT_STATE] = TSConst.DISABLED;
            telnetStateCLE[TSConst.NEG_STATE] = TSConst.TELNET_NEG_AT_DEFAULT;
            telnetStateSGA[TSConst.OPT_STATE] = TSConst.DISABLED;
            telnetStateSGA[TSConst.NEG_STATE] = TSConst.TELNET_NEG_AT_DEFAULT;
        }

        /* Method to become a thread for handling interactive sessions. */
        private void _sessionProcessor()
        {
            // Set the session variables to their proper initial value.
            cmdBufWritePos = 0;
            optBufWritePos = 0;
            echoTyping = false;
            readMode = TSConst.MODE_NORMAL_CMD;
            previousReadMode = TSConst.MODE_NORMAL_CMD;

            // Reset this instance's telnet options to the default state.
            _resetTelnetOptionState();

            // Get the initial telnet options from telnet clients that send them.
            if (activeConnection.Available > 0)
            {
                readByteCount = activeConnection.Receive(receiveBuffer, TSConst.RECEIVE_BUF_SIZE, SocketFlags.None);

                // Process the initial codes.
                if (!_processReceivedData())
                {
                    // Connection negotiation failed or connection is closing.
                    connectionOpen = false;
                    activeConnection.Close();
                    activeConnection = null;
                    available = true;
                    return;
                }
            }

            // Negotiate perferred options if still necessary.

            // Enable suppress goahead option on the remote client. RFC854 says the client cannot reject this option.
            if (telnetStateSGA[TSConst.NEG_STATE] == TSConst.TELNET_NEG_AT_DEFAULT)
            {
                _sendCommand(TSConst.TELNET_WILL, 3);
                telnetStateSGA[TSConst.NEG_STATE] = TSConst.TELNET_NEG_IN_PROGRESS;
            }

            // Tell the remote client that we will echo.
            if (telnetStateCLE[TSConst.NEG_STATE] == TSConst.TELNET_NEG_AT_DEFAULT)
            {
                _sendCommand(TSConst.TELNET_WILL, 1);
                telnetStateCLE[TSConst.NEG_STATE] = TSConst.TELNET_NEG_IN_PROGRESS;
            }

            // Wait for negotiation responses.
            //while ((telnetStateCLE[TSConst.NEG_STATE] < TSConst.TELNET_NEG_COMPLETE) || (telnetStateSGA[TSConst.NEG_STATE] < TSConst.TELNET_NEG_COMPLETE))
            while (telnetStateSGA[TSConst.NEG_STATE] < TSConst.TELNET_NEG_COMPLETE)
            {
                if (activeConnection.Available > 0)
                {
                    readByteCount = activeConnection.Receive(receiveBuffer, TSConst.RECEIVE_BUF_SIZE, SocketFlags.None);

                    // Process the initial codes.
                    if (!_processReceivedData())
                    {
                        // Connection negotiation failed or connection is closing.
                        connectionOpen = false;
                        activeConnection.Close();
                        activeConnection = null;
                        available = true;
                        return;
                    }
                }
            }

            // Finally, negotiations are complete. Start authentication.
            _sendReply("\nUser Access Verification\n\r\n\rPassword: ");

            // Loop will be ended via Thread return after the connection is closed.
            while (connectionOpen)
            {
                // Wait for user input, but only until the timeout period is reached.
                activeConnection.Poll((sessionTimeout * 1000000), SelectMode.SelectRead);

                // If the poll returned and nothing is available to read, it either timed out or the connection was closed.
                if (activeConnection.Available == 0) break;

                // We Got some data... Do something with it.
                do
                {
                    // Read the data.
                    readByteCount = activeConnection.Receive(receiveBuffer, TSConst.RECEIVE_BUF_SIZE, SocketFlags.None);

                    // *** Data comes across as text-equivalent bytes. No UTF-8 decode needed.

                    // This method is only broken out for clarity of code.
                    if (!_processReceivedData())
                    {
                        connectionOpen = false; // Drop out of the outer loop as well.
                        break;
                    }

                } while (activeConnection.Available > 0);

            }

            // Socket state needs to be closed.

            activeConnection.Close();
            activeConnection = null;
            available = true;
            return;

        }

        /* This method appends the received data to the command buffer.             */
        /* The command buffer is necessary in case the command comes in fragmented. */
        private bool _processReceivedData()
        {
            // "Copy" the characters from the receive buffer to the command buffer and check for command completion.
            for (int i = 0; i < readByteCount; i++)
            {
                // Check the current read mode.
                switch (readMode)
                {
                    case TSConst.MODE_SESSION_CMD: // This is just the operation type.
                        if (receiveBuffer[i] > 250)
                        {
                            // WILL, WONT, DO, or DONT operation types have a single option.
                            sessionCmd[0] = receiveBuffer[i];
                            readMode = TSConst.MODE_SESSION_CMD_CODE;
                            continue;
                        }

                        if (receiveBuffer[i] == 250)
                        {
                            // Subnegotiation is starting.
                            previousReadMode = readMode;
                            readMode = TSConst.MODE_SESSION_OPTION;
                            // Set the option buffer write position to the proper starting slot.
                            optBufWritePos = 0;
                            continue;
                        }

                        if (receiveBuffer[i] > 240)
                        {
                            // These operation types have no options.
                            sessionCmd[0] = receiveBuffer[i];
                            sessionCmd[1] = 0; // Bit of a hack, but keeps the loop working great!
                            _processSessionCommand();
                            readMode = previousReadMode;
                            continue;
                        }

                        if (receiveBuffer[i] == 240)
                        {
                            // Subnegotiation is stopping.
                            _processSessionCommandOption();
                            previousReadMode = readMode;
                            readMode = TSConst.MODE_NORMAL_CMD;
                            continue;
                        }

                        // Ingore lower options as they are not yet defined.
                        continue;

                    case TSConst.MODE_SESSION_CMD_CODE: // Grab the actual option and execute.
                        sessionCmd[1] = receiveBuffer[i];
                        _processSessionCommand();
                        readMode = previousReadMode;
                        continue;

                    case TSConst.MODE_SESSION_OPTION:
                        // Will the option buffer overflow?
                        if (optBufWritePos > TSConst.OPTION_BUF_SIZE)
                        {
                            optBufWritePos = 0;
                            _sendReply(TSConst.ERR_LONG_SESSION_CMD);
                            _sendPrompt();
                            return true;
                        }

                        switch (receiveBuffer[i])
                        {
                            case 255: // Interpret as command... Call the session command handler.
                                previousReadMode = readMode;
                                readMode = TSConst.MODE_SESSION_CMD;
                                _processSessionCommand();
                                continue;

                            default: // Stack the data onto the subnegotiation buffer... Haven't read RFC 861 yet...
                                // Add the data to the option buffer.
                                optionBuffer[optBufWritePos] = receiveBuffer[i];
                                // Advance the session command buffer write position.
                                optBufWritePos++;
                                continue;

                        }

                    default: // Treat it as MODE_NORMAL_CMD
                        // Will the command buffer overflow?
                        if (cmdBufWritePos > TSConst.COMMAND_BUF_SIZE)
                        {
                            cmdBufWritePos = 0;
                            _sendReply(TSConst.ERR_LONG_CMD);
                            _sendPrompt();
                            return true;
                        }

                        // Check out the current byte.
                        switch (receiveBuffer[i])
                        {
                            case 255: // A command is going to follow. Switch modes and continue.
                                previousReadMode = readMode;
                                readMode = TSConst.MODE_SESSION_CMD;
                                continue;

                            case 13: // Line feed. Skip it... Activate on carriage return.
                                continue;

                            case 10: // Carriage return.
                                // Advance the line first (without re-sending the command prompt).
                                _sendReply("\n\r");
                                if (!_processCommand(cmdBufWritePos)) return false; // False indicates connection close requested.

                                // Command was handled - Reset the command buffer position so further entries land at the beginning.
                                cmdBufWritePos = 0;
                                continue;

                            case 8: // Backspace key.
                            case 127: // Delete key - Sometimes sent when the backspace key is pressed.
                                // Back up only if possible... Do nothing if we are at the beginning of the prompt.
                                if (cmdBufWritePos > 0)
                                {
                                    // Back up the buffer.
                                    cmdBufWritePos--;
                                    // And if we saw a backspace, we also have to send it back to the client and delete the character!
                                    if (echoTyping) _sendByteArray(new Byte[3] { 8, 32, 8 }); // Back up, send a spece, back up again.
                                }
                                continue;

                            case 9: // Tab key.
                                //Program.processCmd.processCommand(new String(cmdBuffer, 0, cmdBufWritePos));
                                // Do nothing for Tab right now...
                                continue;

                            case 27: // Escape sequence received.
                                // Do nothing for now.
                                i += 2; // Skip over the rest of the escape sequence.
                                continue;

                            default: // No action key received. Append the byte to the command buffer as a character.
                                // Make sure it is a valid character first. *** This will have to be fixed for other languages!
                                if (receiveBuffer[i] < 128)
                                {
                                    // This doesn't support extended ASCII characters at the moment.
                                    cmdBuffer[cmdBufWritePos] = Convert.ToChar(receiveBuffer[i]);
                                }

                                // Advance the command buffer write position.
                                cmdBufWritePos++;

                                // Echo the character back unless it should be hidden.
                                if (echoTyping) _sendByte(receiveBuffer[i]);
                                continue;
                        }
                }
            }
            return true;
        }

        /* This method processes individual commands from the command buffer.          */
        /* True indicates command processed successfully. False closes the connection. */
        private bool _processCommand(int commandLength)
        {
            // Grab the command and change it to a string for simplicity's sake.
            String commandLine = new String(cmdBuffer, 0, commandLength);

            // Are we authenticated?
            if (!authenticated)
            {
                // This can only be a password.

                if (Program.sysConfig.checkAuthPassword(commandLine))
                {
                    // Password is correct!
                    authenticated = true;
                    echoTyping = true;
                    _sendReply("\r\n");
                    _sendPrompt();
                    return true;
                }

                // Wrong password.
                Thread.Sleep(Program.sysConfig.authRetryDelay);
                authTries++; // That was a try...

                if (authTries >= Program.sysConfig.authRetries)
                {
                    _sendReply(TSConst.ERR_BAD_AUTH);
                    Thread.Sleep(TSConst.FAIL_KICK_DELAY);
                    return false; // Too many password attempts... End the session.
                }
                else
                {
                    _sendReply("Password: ");
                    return true; // They get to try again...
                }
            }

            // Split it up into command and arguments.
            String[] command = commandLine.Split(' ');

            // Pick the command and execute it.
            switch (command[0].ToLower())
            {
                /*******************************************************************************************/
                /* All the hard work is done... This part is pretty self-explanatory.                      */
                /* This is where the entered commands are handled. Update as necessary.                    */
                /*******************************************************************************************/
                case "led":
                    if (command.Length > 1) // This command takes 1 argument.
                    {
                        switch (command[1].ToLower()) // What do we do with the LED?
                        {
                            case "on":
                                Program.sysConfig.onBoardLED = true;
                                Program.LEDState = false;
                                _sendReply("The LED has been turned on.\r\n\n");
                                _sendPrompt();
                                break;
                            case "off":
                                Program.sysConfig.onBoardLED = false;
                                _sendReply("The LED has been turned off.\r\n\n");
                                _sendPrompt();
                                break;
                            case "ver 1":
                                Program.LEDMode = 1;
                                _sendReply("The LED Variant mode has been set to 1.\r\n\n");
                                _sendPrompt();
                                break;
                            case "ver 2":
                                Program.LEDMode = 2;
                                _sendReply("The LED Variant mode has been set to 2.\r\n\n");
                                _sendPrompt();
                                break;
                            case "ver 3":
                                Program.LEDMode = 3;
                                _sendReply("The LED Variant mode has been set to 3.\r\n\n");
                                _sendPrompt();
                                break;
                            case "ver 4":
                                Program.LEDMode = 4;
                                _sendReply("The LED Variant mode has been set to 4.\r\n\n");
                                _sendPrompt();
                                break;
                            default:
                                _sendReply("Please specify 'on' or 'off'.\r\n\n");
                                _sendPrompt();
                                break;
                        }
                    }
                    else // Not enough arguments!
                    {
                        _sendReply("Please specify 'on' or 'off'.\r\n\n");
                        _sendPrompt();
                    }

                    break;

                case "show": // Show system information.
                    if (command.Length > 1) // This command takes 1 argument.
                    {
                        switch (command[1].ToLower()) // What dowe want to display?
                        {
                            case "ver":
                                _sendReply("Telnet Server Software version " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString() + "\r\n\n");
                                _sendPrompt();
                                break;

                            case "stat":
                                _sendReply("Fetching status information...\n\r");
                                _sendReply(_sysStatus());
                                _sendPrompt();
                                break;

                            case "run":
                                _sendReply("Building configuration...\n\r");
                                _sendReply(Program.sysConfig.getRunningConfig);
                                _sendPrompt();
                                break;

                            case "start":
                                _sendReply("Building configuration...\n\r");
                                _sendReply(Program.sysConfig.getStartupConfig);
                                _sendPrompt();
                                break;

                            default:
                                _sendReply("Please specify 'run', 'start', 'stat', or 'ver'.\r\n\n");
                                _sendPrompt();
                                break;
                        }
                    }
                    else // Not enough arguments!
                    {
                        _sendReply("Please specify 'on' or 'off'.\r\n\n");
                        _sendPrompt();
                    }

                    break;

                case "version": // Print the version number.


                case "exit": // Got to have a way out, right?
                    return false;

                default:
                    _sendReply("Available commands are:\r\n\n" +
                               "led <on or off or var n> - Turn on-board LED on or off\r\n" +
                               "show <run, start, stat, ver> - Displays various system information.\r\n" +
                               "exit - End the connection.\r\n\n");
                    _sendPrompt();
                    break;
                /*******************************************************************************************/
            }

            return true;
        }

        /* This method processes normal session commands from the sessionCmd buffer. */
        /* True indicates session command processed successfully. False closes the connection.  */
        private bool _processSessionCommand()
        {
            switch (sessionCmd[1])
            {
                case 0: // One of the defined optionless commands came in.
                    switch (sessionCmd[0])
                    {
                        case 243: // Break command is watched so the user can abort if the telnet client is buggy.
                            // Drop the option string and return to the command prompt.
                            /************************************************************************/
                            /* You'll have to actually abort whatever is going on with code here... */
                            /************************************************************************/
                            previousReadMode = readMode;
                            readMode = TSConst.MODE_NORMAL_CMD;
                            _sendReply(TSConst.ERR_BREAK);
                            _sendPrompt();
                            return true;

                        case 244: // Interrupt process - Typically Control-C was pressed.
                            // Drop the option string and return to the command prompt.
                            /************************************************************************/
                            /* You'll have to actually abort whatever is going on with code here... */
                            /************************************************************************/
                            previousReadMode = readMode;
                            readMode = TSConst.MODE_NORMAL_CMD;
                            _sendReply(TSConst.ERR_INTERRUPT);
                            _sendPrompt();
                            return true;

                        case 246: // Are you There? Send response to the client so it knows the connection is still active.
                            _sendCommand(241); // Send NOOP as acknowledgement.
                            return true;

                        default: // Not implementing any other optionless commands at this time. Ignore them.
                            return true;

                    }

                case 1: // Client local echo option received.
                    // Is this a continuation of a negotiation we started or a new one at the behest of the client?
                    if (telnetStateCLE[TSConst.NEG_STATE] == TSConst.TELNET_NEG_IN_PROGRESS)
                    {
                        // DO and DONT are broken responses here... But watch for them anyway.
                        if ((sessionCmd[0] == TSConst.TELNET_WILL) || (sessionCmd[0] == TSConst.TELNET_DO)) // We made the request, it was accepted!
                        {
                            telnetStateCLE[TSConst.OPT_STATE] = TSConst.ENABLED;
                        }

                        if ((sessionCmd[0] == TSConst.TELNET_WONT) || (sessionCmd[0] == TSConst.TELNET_DONT)) // We made the request, it was refused.
                        {
                            telnetStateCLE[TSConst.OPT_STATE] = TSConst.DISABLED;
                        }
                    }
                    // The other client is starting the conversation.
                    else
                    {
                        if (sessionCmd[0] == TSConst.TELNET_WILL) // Client asking to be allowed to echo.
                        {
                            _sendCommand(TSConst.TELNET_DONT, 1); // Tell the client not to.
                            telnetStateCLE[TSConst.OPT_STATE] = TSConst.ENABLED;
                        }

                        if (sessionCmd[0] == TSConst.TELNET_WONT) // Client is stating it can not perform local echo.
                        {
                            _sendCommand(TSConst.TELNET_DONT, 1); // Tell the client it is ok not to echo.
                            telnetStateCLE[TSConst.OPT_STATE] = TSConst.DISABLED;
                        }

                        if (sessionCmd[0] == TSConst.TELNET_DO) // Client asking us to echo.
                        {
                            _sendCommand(TSConst.TELNET_WILL, 1); // Tell the client we will echo.
                            telnetStateCLE[TSConst.OPT_STATE] = TSConst.ENABLED;
                        }

                        if (sessionCmd[0] == TSConst.TELNET_DONT) // Client is stating it wants us to not echo.
                        {
                            _sendCommand(TSConst.TELNET_WILL, 1); // Tell the client we will echo.
                            telnetStateCLE[TSConst.OPT_STATE] = TSConst.ENABLED;
                        }
                    }

                    // In any case, the negotiation is now complete.
                    telnetStateCLE[TSConst.NEG_STATE] = TSConst.TELNET_NEG_COMPLETE;

                    return true;

                case 3: // Suppress goahead option received.
                    // Is this a continuation of a negotiation we started or a new one at the behest of the client?
                    if (telnetStateSGA[TSConst.NEG_STATE] == TSConst.TELNET_NEG_IN_PROGRESS)
                    {
                        // DO and DONT are broken responses here... But watch for them anyway.
                        if ((sessionCmd[0] == TSConst.TELNET_WILL) || (sessionCmd[0] == TSConst.TELNET_DO)) // We made the request, it was accepted!
                        {
                            telnetStateSGA[TSConst.OPT_STATE] = TSConst.ENABLED;
                        }

                        if ((sessionCmd[0] == TSConst.TELNET_WONT) || (sessionCmd[0] == TSConst.TELNET_DONT)) // We made the request, it was refused.
                        {
                            telnetStateSGA[TSConst.OPT_STATE] = TSConst.DISABLED;
                        }
                    }
                    // The other client is starting the conversation.
                    else
                    {
                        if (sessionCmd[0] == TSConst.TELNET_WILL) // Client asking to be allowed to suppress goahead.
                        {
                            _sendCommand(TSConst.TELNET_DO, 3); // Tell the client it is ok to suppress goahead.
                            telnetStateSGA[TSConst.OPT_STATE] = TSConst.ENABLED;
                        }

                        if (sessionCmd[0] == TSConst.TELNET_WONT) // Client is stating it can not suppress goahead.
                        {
                            _sendCommand(TSConst.TELNET_DONT, 3); // Tell the client it is ok not to suppress goahead.
                            telnetStateSGA[TSConst.OPT_STATE] = TSConst.DISABLED;
                        }

                        if (sessionCmd[0] == TSConst.TELNET_DO) // Client asking us to suppress goahead.
                        {
                            _sendCommand(TSConst.TELNET_WILL, 3); // Tell the client we will suppress goahead.
                            telnetStateSGA[TSConst.OPT_STATE] = TSConst.ENABLED;
                        }

                        if (sessionCmd[0] == TSConst.TELNET_DONT) // Client is stating it wants us to not suppress goahead.
                        {
                            _sendCommand(TSConst.TELNET_WONT, 3); // Tell the client we will not suppress goahead.
                            telnetStateSGA[TSConst.OPT_STATE] = TSConst.DISABLED;
                        }
                    }

                    // In any case, the negotiation is now complete.
                    telnetStateSGA[TSConst.NEG_STATE] = TSConst.TELNET_NEG_COMPLETE;

                    return true;

                default:
                    // Ignore other options as per RFC854.
                    if ((sessionCmd[0] == TSConst.TELNET_WILL) || (sessionCmd[0] == TSConst.TELNET_WONT))
                    {
                        _sendCommand(TSConst.TELNET_WONT, sessionCmd[1]);
                    }
                    else
                    {
                        _sendCommand(TSConst.TELNET_DONT, sessionCmd[1]);
                    }

                    return true;
            }
        }

        /* This method processes extended session commands and options from the optionBuffer command buffer. */
        /* True indicates session command processed successfully. False closes the connection.  */
        private bool _processSessionCommandOption()
        {
            // We're not handling any extended options yet...
            return true;
        }

        /* Properly encodes and sends the passed string. */
        private void _sendReply(String strToSend)
        {
            activeConnection.Send(Encoding.UTF8.GetBytes(strToSend), SocketFlags.None);
        }

        /* Sends the current command prompt. */
        private void _sendPrompt()
        {
            if (enabled)
            {
                activeConnection.Send(Encoding.UTF8.GetBytes(Program.sysConfig.hostname + "#"), SocketFlags.None);
            }
            else
            {
                activeConnection.Send(Encoding.UTF8.GetBytes(Program.sysConfig.hostname + ">"), SocketFlags.None);
            }
        }

        /* Send the specified command and option code. */
        private void _sendCommand(byte cmd, byte code = 0)
        {
            sendingCmd[0] = 255; // Always start with the IAC (Interpret As Command) byte.
            sendingCmd[1] = cmd;
            sendingCmd[2] = code;

            if (code == 0)
                activeConnection.Send(sendingCmd, 2, SocketFlags.None); // Send command only.
            else
                activeConnection.Send(sendingCmd, 3, SocketFlags.None); // Send command and option.

        }

        /* Send a byte as-is. */
        private void _sendByte(byte byteToSend)
        {
            byteToByteArray[0] = byteToSend;
            activeConnection.Send(byteToByteArray);
        }

        /* Send a byte array as-is. */
        private void _sendByteArray(byte[] byteArrayToSend)
        {
            activeConnection.Send(byteArrayToSend);
        }

        /* Build up and return system status information. */
        private string _sysStatus()
        {
            // Return value to build up.
            string status = "";

            // I/O Status section
            status += "============\n\r";
            status += " I/O Status\n\r";
            status += "============\n\r";

            // Get the on-board LED status.
            status += "OnBoard LED is currently: ";
            if (Program.sysConfig.onBoardLED)
                status += "On";
            else
                status += "Off";
            status += "\n\r";

            // Network status section
            status += "\n\r";
            status += "================\n\r";
            status += " Network Status\n\r";
            status += "================\n\r";

            // Get the running Telnet sessions.
            status += "Active Telnet sessions: " + Program.telnetSvr.getTelnetSessionCount + "\n\r";

            // Get the Ethernet configuration.
            status += "\n\r";
            status += "MAC Address: " + Program.sysConfig.MAC + "\n\r";
            status += " IP Address: " + Program.sysConfig.IPAddress + "\n\r";
            status += "Subnet Mask: " + Program.sysConfig.SubNetMask + "\n\r";
            status += "    Gateway: " + Program.sysConfig.Gateway + "\n\r";

            // System status section
            status += "\n\r";
            status += "===============\n\r";
            status += " System Status\n\r";
            status += "===============\n\r";

            // Get the amount of free RAM.
            status += "Free system memory: " + Debug.GC(true) + "\n\r";

            // That's everything... For now.
            return status;

        }
    }
}
